package interceptcrossing.ui;

import interceptcrossing.Intercept;
import interceptcrossing.Intersection;
import interceptcrossing.Point;
import interceptcrossing.ui.command.AddInterceptCommand;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.*;
import java.util.List;

public class PaintPanel extends JPanel implements MainWindowDirector.Listener {

	public static enum Mode {

		SELECTING,
		ADDING
	}

	private MainWindowDirector windowDirector;

	private List<Intercept> intercepts;
	private Set<Intersection> intersections;
	private Intercept currentIntercept;
	private SelectingMouseAdapter selectingMouseListener;
	private AddingMouseAdapter addingMouseListener;
	private Mode mode;
	private InterceptPaintUtils paintUtils;

	public PaintPanel(MainWindowDirector windowDirector) {
		this.windowDirector = windowDirector;
		this.windowDirector.addListener(this);

		this.intercepts = new ArrayList<Intercept>();
		this.intersections = new HashSet<Intersection>();
		this.currentIntercept = null;

		this.addingMouseListener = new AddingMouseAdapter();
		this.selectingMouseListener = new SelectingMouseAdapter();

		this.paintUtils = new InterceptPaintUtils(this);

		this.setMode(Mode.SELECTING);
	}

	public void setMode(Mode mode) {
		this.mode = mode;

		List<MouseListener> listeners = Arrays.asList(super.getMouseListeners());
		List<MouseMotionListener> motionListeners
			= Arrays.asList(super.getMouseMotionListeners());

		switch (mode) {
			case ADDING:
				if (listeners.contains(this.selectingMouseListener)) {
					super.removeMouseListener(this.selectingMouseListener);
				}

				if (!listeners.contains(this.addingMouseListener)) {
					super.addMouseListener(this.addingMouseListener);
				}

				if (!motionListeners.contains(this.addingMouseListener)) {
					super.addMouseMotionListener(this.addingMouseListener);
				}

				break;
			case SELECTING:
				if (listeners.contains(this.addingMouseListener)) {
					super.removeMouseListener(this.addingMouseListener);
				}

				if (motionListeners.contains(this.addingMouseListener)) {
					super.removeMouseMotionListener(this.addingMouseListener);
				}

				if (!listeners.contains(this.selectingMouseListener)) {
					super.addMouseListener(this.selectingMouseListener);
				}
				break;
		}
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);

		Graphics2D g2d = (Graphics2D) g;

		this.paintUtils.paintBackground(g2d);
		this.paintUtils.paintAxises(g2d);

		if (this.currentIntercept == null) {
			this.paintUtils.paintIntercepts(g2d, this.intercepts);
		} else {
			this.paintUtils.paintIntercepts(g2d, this.intercepts, this.currentIntercept);
		}

		this.paintUtils.paintIntersections(g2d, this.intersections);

		if (this.mode.equals(Mode.ADDING) && this.addingMouseListener.isMousePressed()) {
			int x1 = this.addingMouseListener.x();
			int y1 = this.addingMouseListener.y();
			int x2 = this.addingMouseListener.mouseX();
			int y2 = this.addingMouseListener.mouseY();

			this.paintUtils.paintAddingIntercept(g2d, x1, y1, x2, y2);
		}
	}

	/*private void paintAddingIntercept(Graphics2D g) {
		if (this.mode.equals(Mode.ADDING) && this.addingMouseListener.isMousePressed()) {
			//
		}
	}*/

	public void interceptAdded(Intercept intercept) {
		this.intersections.clear();
		this.intercepts.add(intercept);
		this.paintUtils.recalculateBounds(this.intercepts);

		super.repaint();
	}

	public void interceptRemoved(Intercept intercept) {
		this.intersections.clear();
		if (this.intercepts.contains(intercept)) {
			this.intercepts.remove(intercept);
		}

		this.paintUtils.recalculateBounds(this.intercepts);

		super.repaint();
	}

	public void interceptsCleared() {
		this.intercepts.clear();
		this.paintUtils.recalculateBounds(this.intercepts);

		super.repaint();
	}

	public void intersectionsFound(Set<Intersection> intersections) {
		for (Intersection intersection : intersections) {
			if (!this.intersections.contains(intersection)) {
				this.intersections.add(intersection);
			}
		}

		super.repaint();
	}

	public void currentInterceptChanged(int index, Intercept intercept) {
		this.currentIntercept = intercept;
		super.repaint();
	}

	private class SelectingMouseAdapter extends MouseAdapter {

		@Override
		public void mouseClicked(MouseEvent e) {
			Point click = PaintPanel.this.paintUtils.fromComponentCoords(e.getX(), e.getY());

			double minDistance = Double.MAX_VALUE;
			Intercept current = null;

			for (Intercept intercept : PaintPanel.this.intercepts) {
				double distance = this.getMinDistance(click, intercept);

				if (distance < minDistance) {
					current = intercept;
					minDistance = distance;
				}
			}

			PaintPanel.this.windowDirector.changeCurrentIntercept(current);
		}

		// Вычисление длины перпендикуляра, опущенного из точки к отрезку
		private double perpendicular(Point point, Intercept intercept) {
			/*
			 * Строим треугольник со вершинами A, B, C.
			 * C - заданная точка, A и B - начало и конец заданного отрезка.
			 * Из точки C опускаем перпендикуляр к стороне AB.
			 * Его длина совпадает со значением высоты треугольника.
			 *
			 * Hc = 2 * Sabc / AB
			 * p = (AB + BC + AC) / 2
			 * Sabc = sqrt(p * (p - AB) * (p - AC) * (p - BC))
			 */

			double ab = intercept.length();
			double ac = point.distanceTo(intercept.begin());
			double bc = point.distanceTo(intercept.end());
			double p = (ab + bc + ac) / 2;
			double s = Math.sqrt(p * (p - ab) * (p - bc) * (p - ac));

			return (2 * s) / ab;
		}

		private double getMinDistance(Point point, Intercept intercept) {
			/*
			 * Если перпендикуляр опускается не на сам отрезок,
			 * а на продолжение его прямой, то берём расстояние от заданной
			 * точки до ближайшего конца отрезка, иначе сам перпендикуляр.
			 */

			double ab = intercept.length();
			double ac = point.distanceTo(intercept.begin());
			double bc = point.distanceTo(intercept.end());
			double ch = this.perpendicular(point, intercept);

			double ah = Math.sqrt(ac * ac - ch * ch);
			double bh = Math.sqrt(bc * bc - ch * ch);

			double ret = ch;

			if ((ah + bh) - ab > 0.001) {
				ret = (ac < bc) ? ac : bc;
			}

			return ret;
		}
	}

	private class AddingMouseAdapter implements MouseListener, MouseMotionListener {

		private int x;
		private int y;
		private int mouseX;
		private int mouseY;
		private boolean mousePressed;

		public void mouseClicked(MouseEvent e) {
		}

		public void mouseEntered(MouseEvent e) {
		}

		public void mouseExited(MouseEvent e) {
		}

		public void mouseDragged(MouseEvent e) {
			this.mouseX = e.getX();
			this.mouseY = e.getY();
			PaintPanel.this.repaint();
		}

		public void mouseMoved(MouseEvent e) {
		}

		public void mousePressed(MouseEvent e) {
			this.x = e.getX();
			this.y = e.getY();
			this.mousePressed = true;
		}

		public void mouseReleased(MouseEvent e) {
			this.mousePressed = false;

			Point begin = PaintPanel.this.paintUtils.fromComponentCoords(this.x, this.y);
			Point end = PaintPanel.this.paintUtils.fromComponentCoords(e.getX(), e.getY());

			windowDirector.getCommandManager().doCommand(
				new AddInterceptCommand(windowDirector, new Intercept(begin, end)));
		}

		public boolean isMousePressed() {
			return mousePressed;
		}

		public int mouseX() {
			return this.mouseX;
		}

		public int mouseY() {
			return this.mouseY;
		}

		public int x() {
			return this.x;
		}

		public int y() {
			return this.y;
		}
	}
}
